Хотя NumPy построен на языке С, некоторые вычислительно интенсивные алгоритмы сталкиваются с стеной векторизации. Это происходит тогда, когда присущая задержка динамической природы Python превышает преимущества высокого уровня абстракции.
1. Налог интерпретатора и упаковка
Каждая итерация в стандартном цикле Python включает динамическую проверку типов и подсчет ссылок. Даже при использовании скаляров NumPy «упаковка» исходных данных С в объекты Python создает огромный узкий проход для функций, таких как $\text{logit}(p) = \log(p/(1-p))$. Обработка граничных случаев на С значительно быстрее:
>>> logit(0) -> -бесконечность >>> logit(1) -> бесконечность >>> logit(2) -> неопределённое значение (nan) >>> logit(-2) -> неопределённое значение (nan)
2. Рост промежуточных массивов
Чистые выражения NumPy создают временные буферы памяти для каждой подоперации. Расширение через C-API позволяет использовать объединение ядра, где преобразование logit рассчитывается за один проход без дополнительной нагрузки на память.
3. Пространственные зависимости
Операции, связанные с паттернами доступа к соседям, такие как двумерная шаблонная операция:
$$B(I, J) = A(I, J) + (A(I-1, J) + A(I+1, J) + A(I, J-1) + A(I, J+1)) \cdot 0.5D0 + (A(I-1, J-1) + A(I-1, J+1) + A(I+1, J-1) + A(I+1, J+1)) \cdot 0.25D0$$
их сложно эффективно выразить с помощью срезов без избыточных копий памяти. Расширения на С позволяют выполнять прямую арифметику указателей с выравниванием по кэшу.